package code.google.jcustomize;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.FunctionMapper;
import javax.servlet.jsp.el.VariableResolver;

import org.apache.commons.el.ExpressionEvaluatorImpl;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

class PropertyConfig extends AbstractConfig {
    private boolean USE_FS_PATH     = false;
    private Map items = new LinkedHashMap();
    private String path; 
    private boolean casesens = true;

    public boolean patch(ConfigSource cs, VariableResolver variableResolver, FunctionMapper functionMapper) {
        String tempFileName = cs.createTempFile(path, "cfg"+getName().charAt(0), USE_FS_PATH);
        try {
            BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(cs.fileConfigWrite(tempFileName, false, true)), "utf8"));
            BufferedReader r = new BufferedReader(new InputStreamReader(new BufferedInputStream(cs.fileConfigRead(path, USE_FS_PATH)), "utf8"));
            boolean result = patch(r, w, variableResolver, functionMapper);
            w.flush();
            w.close();
            r.close();
            if (result)
                cs.safeRename(tempFileName, path, true, USE_FS_PATH);
            return result;
        } catch (FileNotFoundException fnfe) {
            fnfe.printStackTrace();
        } catch (UnsupportedEncodingException uee) {
            uee.printStackTrace();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
        return false;
    }

    private boolean patch(BufferedReader r, BufferedWriter w, VariableResolver variableResolver, FunctionMapper functionMapper) {
        ExpressionEvaluator ee = new ExpressionEvaluatorImpl();
        try {
            boolean breakedLine = false;
            boolean changed = false;
            while (true) {
                String line = r.readLine();
                if (line == null) break;
                if (line.length() == 0) {
                    if (!(breakedLine && changed)) w.newLine();
                    breakedLine = changed = false;
                    continue;
                }
                
                int slashes = 0;
                for (int i = line.length()-1; i >= 0; i--) 
                    if (line.charAt(i) == '\\') slashes++; else break;
                boolean currentLineIsBreaked = (slashes & 1) == 1;
                
                String tmp = line.trim();
                int keyend   = -1;
                int valstart = -1;
                
                boolean skip = breakedLine || tmp.length() == 0 || tmp.charAt(0) == '#' || tmp.charAt(0) == '!';
                if (!skip) {
                    int sl = 0;
                    boolean leadingWhiteSpace = true;
                    for (int i = 0; i < line.length(); i++) {
                        final char c = line.charAt(i);
                        if (c == '\\') {
                            sl++; continue;
                        } 
                        else if (c == '=' || c == ':' || c == ' ' || c == '\t') {
                            if ((sl & 1) == 0) {
                                if (keyend < 0 && !leadingWhiteSpace) keyend = i;
                            }
                        } else {
                            if (keyend >= 0) {
                                valstart = i; break;
                            }
                            leadingWhiteSpace = false;
                        }
                        sl = 0;
                    }
                    skip = !(keyend > 0 && valstart > 0);
                }
                if (skip) {
                    if (!(breakedLine && changed)) {
                        w.write(line);
                        w.newLine();
                    }
                    breakedLine = currentLineIsBreaked;
                    changed = changed && currentLineIsBreaked;
                    continue;
                }
                
                String keyword = line.substring(0, keyend).trim();
                if (keyword != null && !casesens)
                    keyword = keyword.toLowerCase();
                
                ConfigItem item = (ConfigItem)items.get(keyword);
                if (item == null) {
                    w.write(line);
                    w.newLine();
                    breakedLine = currentLineIsBreaked;
                    changed = false;
                    continue;
                }
                if (item.getAction() == ConfigItem.ACTION_REMOVE) {
                    breakedLine = currentLineIsBreaked;
                    changed = true;
                    continue;
                }

                String value = item.getValue();
                for (Iterator itv = item.getConditionVals().iterator(), itt = item.getConditionTests().iterator(); itv.hasNext() && itt.hasNext(); ) {
                    String v = (String)itv.next();
                    String t = (String)itt.next();
                    Boolean cond = (Boolean) ee.evaluate(t, Boolean.class, variableResolver, functionMapper);
                    if (cond != null && cond.booleanValue()) {
                        value = v; break;
                    }
                }
                if (value.indexOf("${") >= 0) {
                    value = (String)ee.evaluate(value, String.class, variableResolver, functionMapper);
                    if (value== null && item.isRequired()) throw new RuntimeException("Can't evaluate required value for '"+item.getPath()+"', config '"+getName()+"'");
                }

                if (item.getReplace() != null) {
                    String current = line.substring(valstart);
                    value = (current != null) ? current.replaceAll(item.getReplace(), value) : null;
                }
                
                StringBuffer sb = new StringBuffer(value != null ? value.length()*6+2: 16);
                if (value != null)
                    for (int i=0; i < value.length(); i++) {
                        final char c = value.charAt(i);
                        if (c < 0x0020 || c > 0x007E) {
                            sb.append("\\u");
                            sb.append(toHex(c >> 12)).append(toHex(c >> 8)).append(toHex(c >> 4)).append(toHex(c));
                        } else sb.append(c);
                    }
                w.write(line.substring(0, keyend).trim()+"="+sb.toString());
                w.newLine();
                changed = true;
                breakedLine = currentLineIsBreaked;
            }
            return true;
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } catch (ELException ele) {
            ele.printStackTrace();
        }
        return false;
    }

    private static char toHex(int nibble) {
        return hexDigit[(nibble & 0xF)];
    }

    private static final char[] hexDigit = {
        '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
    };

    protected void readItems(Node config) {
        Element cfg = (Element)config;
        path = cfg.getAttribute("path");
        if ("".equals(path))
            throw new IllegalArgumentException("Path is required for xml-config '"+getName()+"'");
        casesens = !"false".equalsIgnoreCase(((Element)config).getAttribute("c12e"));
        
        for (Node child = config.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() != Node.ELEMENT_NODE) continue;
            if ("item".equals(child.getNodeName())) {
                ConfigItem item = readItem((Element)child);
                if (item != null) items.put(item.getPath(), item);
            }
        }
    }

    private ConfigItem readItem(Element el) {
        ConfigItem item = new ConfigItem();
        for (Node child = el.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() != Node.ELEMENT_NODE) continue;
            final String name = child.getNodeName();
            if ("name".equals(name)) {
                String nm = getElementValue(child);
                item.setPath(casesens ? nm : (nm != null ? nm.toLowerCase() : null));
            }
            else if ("value".equals(name)) {
                String cond = ((Element)child).getAttribute("when");
                if (cond.length() > 0)
                    item.addConditionValue(getElementValue(child), cond);
                else
                    item.setValue(getElementValue(child));
                cond = ((Element)child).getAttribute("replace");
                if (cond.length() > 0) item.setReplace(cond);
            }
            else if ("action".equals(name))
                item.setAction(getElementValue(child));
            else if ("condition".equals(name))
                item.setCondition(getElementValue(child));
        }
        if (item.getPath() == null) return null;
        String req = el.getAttribute("required").toLowerCase();
        item.setRequired("true".equals(req) || "yes".equals(req));
        return item;
    }

    public boolean skip(VariableResolver variableResolver, FunctionMapper functionMapper) {
        if (super.skip(variableResolver, functionMapper)) return true;
        return items.isEmpty();
    }

}
